package org.apache.maven.project.interpolation;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.apache.maven.project.DefaultProjectBuilderConfiguration;
import org.apache.maven.project.ProjectBuilderConfiguration;
import org.apache.maven.project.path.PathTranslator;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.interpolation.AbstractValueSource;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.MapBasedValueSource;
import org.codehaus.plexus.interpolation.ObjectBasedValueSource;
import org.codehaus.plexus.interpolation.PrefixAwareRecursionInterceptor;
import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
import org.codehaus.plexus.interpolation.PrefixedValueSourceWrapper;
import org.codehaus.plexus.interpolation.RecursionInterceptor;
import org.codehaus.plexus.interpolation.ValueSource;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Use a regular expression search to find and resolve expressions within the POM.
 *
 * @author jdcasey Created on Feb 3, 2005
 * @todo Consolidate this logic with the PluginParameterExpressionEvaluator, minus
 *       deprecations/bans.
 */
@Deprecated
public abstract class AbstractStringBasedModelInterpolator extends AbstractLogEnabled
    implements ModelInterpolator, Initializable {
  private static final List<String> PROJECT_PREFIXES = Arrays.asList("pom.", "project.");

  private static final List<String> TRANSLATED_PATH_EXPRESSIONS;

  static {
    List<String> translatedPrefixes = new ArrayList<String>();

    // MNG-1927, MNG-2124, MNG-3355:
    // If the build section is present and the project directory is non-null, we should make
    // sure interpolation of the directories below uses translated paths.
    // Afterward, we'll double back and translate any paths that weren't covered during
    // interpolation via the
    // code below...
    translatedPrefixes.add("build.directory");
    translatedPrefixes.add("build.outputDirectory");
    translatedPrefixes.add("build.testOutputDirectory");
    translatedPrefixes.add("build.sourceDirectory");
    translatedPrefixes.add("build.testSourceDirectory");
    translatedPrefixes.add("build.scriptSourceDirectory");
    translatedPrefixes.add("reporting.outputDirectory");

    TRANSLATED_PATH_EXPRESSIONS = translatedPrefixes;
  }

  @Requirement
  private PathTranslator pathTranslator;

  private Interpolator interpolator;

  private RecursionInterceptor recursionInterceptor;

  // for testing.
  protected AbstractStringBasedModelInterpolator(PathTranslator pathTranslator) {
    this.pathTranslator = pathTranslator;
  }

  /**
   * @todo: Remove the throws clause.
   * @throws IOException This exception is not thrown any more, and needs to be removed.
   */
  protected AbstractStringBasedModelInterpolator() {
  }

  public Model interpolate(Model model, Map<String, ?> context) throws ModelInterpolationException {
    return interpolate(model, context, true);
  }

  /**
   * Serialize the inbound Model instance to a StringWriter, perform the regex replacement to
   * resolve POM expressions, then re-parse into the resolved Model instance. <br/>
   * <b>NOTE:</b> This will result in a different instance of Model being returned!!!
   *
   * @param model The inbound Model instance, to serialize and reference for expression resolution
   * @param context The other context map to be used during resolution
   * @return The resolved instance of the inbound Model. This is a different instance!
   *
   * @deprecated Use
   *             {@link ModelInterpolator#interpolate(Model, File, ProjectBuilderConfiguration, boolean)}
   *             instead.
   */
  public Model interpolate(Model model, Map<String, ?> context, boolean strict) throws ModelInterpolationException {
    Properties props = new Properties();
    props.putAll(context);

    return interpolate(model, null, new DefaultProjectBuilderConfiguration().setExecutionProperties(props), true);
  }

  public Model interpolate(Model model, File projectDir, ProjectBuilderConfiguration config, boolean debugEnabled)
      throws ModelInterpolationException {
    StringWriter sWriter = new StringWriter(1024);

    MavenXpp3Writer writer = new MavenXpp3Writer();
    try {
      writer.write(sWriter, model);
    } catch (IOException e) {
      throw new ModelInterpolationException("Cannot serialize project model for interpolation.", e);
    }

    String serializedModel = sWriter.toString();
    serializedModel = interpolate(serializedModel, model, projectDir, config, debugEnabled);

    StringReader sReader = new StringReader(serializedModel);

    MavenXpp3Reader modelReader = new MavenXpp3Reader();
    try {
      model = modelReader.read(sReader);
    } catch (IOException e) {
      throw new ModelInterpolationException(
          "Cannot read project model from interpolating filter of serialized version.", e);
    } catch (XmlPullParserException e) {
      throw new ModelInterpolationException(
          "Cannot read project model from interpolating filter of serialized version.", e);
    }

    return model;
  }

  /**
   * Interpolates all expressions in the src parameter.
   * <p>
   * The algorithm used for each expression is:
   * <ul>
   * <li>If it starts with either "pom." or "project.", the expression is evaluated against the
   * model.</li>
   * <li>If the value is null, get the value from the context.</li>
   * <li>If the value is null, but the context contains the expression, don't replace the expression
   * string with the value, and continue to find other expressions.</li>
   * <li>If the value is null, get it from the model properties.</li>
   * <li>
   * 
   * @param overrideContext
   * @param outputDebugMessages
   */
  public String interpolate(String src, Model model, final File projectDir, ProjectBuilderConfiguration config,
      boolean debug) throws ModelInterpolationException {
    try {
      List<ValueSource> valueSources = createValueSources(model, projectDir, config);
      List<InterpolationPostProcessor> postProcessors = createPostProcessors(model, projectDir, config);

      return interpolateInternal(src, valueSources, postProcessors, debug);
    } finally {
      interpolator.clearAnswers();
    }
  }

  protected List<ValueSource> createValueSources(final Model model, final File projectDir,
      final ProjectBuilderConfiguration config) {
    String timestampFormat = DEFAULT_BUILD_TIMESTAMP_FORMAT;

    Properties modelProperties = model.getProperties();
    if (modelProperties != null) {
      timestampFormat = modelProperties.getProperty(BUILD_TIMESTAMP_FORMAT_PROPERTY, timestampFormat);
    }

    ValueSource modelValueSource1 = new PrefixedObjectValueSource(PROJECT_PREFIXES, model, false);
    ValueSource modelValueSource2 = new ObjectBasedValueSource(model);

    ValueSource basedirValueSource = new PrefixedValueSourceWrapper(new AbstractValueSource(false) {
      public Object getValue(String expression) {
        if (projectDir != null && "basedir".equals(expression)) {
          return projectDir.getAbsolutePath();
        }
        return null;
      }
    }, PROJECT_PREFIXES, true);
    ValueSource baseUriValueSource = new PrefixedValueSourceWrapper(new AbstractValueSource(false) {
      public Object getValue(String expression) {
        if (projectDir != null && "baseUri".equals(expression)) {
          return projectDir.getAbsoluteFile().toURI().toString();
        }
        return null;
      }
    }, PROJECT_PREFIXES, false);

    List<ValueSource> valueSources = new ArrayList<ValueSource>(9);

    // NOTE: Order counts here!
    valueSources.add(basedirValueSource);
    valueSources.add(baseUriValueSource);
    valueSources.add(new BuildTimestampValueSource(config.getBuildStartTime(), timestampFormat));
    valueSources.add(modelValueSource1);
    valueSources.add(new MapBasedValueSource(config.getUserProperties()));
    valueSources.add(new MapBasedValueSource(modelProperties));
    valueSources.add(new MapBasedValueSource(config.getExecutionProperties()));
    valueSources.add(new AbstractValueSource(false) {
      public Object getValue(String expression) {
        return config.getExecutionProperties().getProperty("env." + expression);
      }
    });
    valueSources.add(modelValueSource2);

    return valueSources;
  }

  protected List<InterpolationPostProcessor> createPostProcessors(final Model model, final File projectDir,
      final ProjectBuilderConfiguration config) {
    return Collections.singletonList((InterpolationPostProcessor) new PathTranslatingPostProcessor(PROJECT_PREFIXES,
        TRANSLATED_PATH_EXPRESSIONS, projectDir, pathTranslator));
  }

  @SuppressWarnings("unchecked")
  protected String interpolateInternal(String src, List<ValueSource> valueSources,
      List<InterpolationPostProcessor> postProcessors, boolean debug) throws ModelInterpolationException {
    if (!src.contains("${")) {
      return src;
    }

    Logger logger = getLogger();

    String result = src;
    synchronized (this) {

      for (ValueSource vs : valueSources) {
        interpolator.addValueSource(vs);
      }

      for (InterpolationPostProcessor postProcessor : postProcessors) {
        interpolator.addPostProcessor(postProcessor);
      }

      try {
        try {
          result = interpolator.interpolate(result, recursionInterceptor);
        } catch (InterpolationException e) {
          throw new ModelInterpolationException(e.getMessage(), e);
        }

        if (debug) {
          List<Object> feedback = interpolator.getFeedback();
          if (feedback != null && !feedback.isEmpty()) {
            logger.debug("Maven encountered the following problems during initial POM interpolation:");

            Object last = null;
            for (Object next : feedback) {
              if (next instanceof Throwable) {
                if (last == null) {
                  logger.debug("", ((Throwable) next));
                } else {
                  logger.debug(String.valueOf(last), ((Throwable) next));
                }
              } else {
                if (last != null) {
                  logger.debug(String.valueOf(last));
                }

                last = next;
              }
            }

            if (last != null) {
              logger.debug(String.valueOf(last));
            }
          }
        }

        interpolator.clearFeedback();
      } finally {
        for (ValueSource vs : valueSources) {
          interpolator.removeValuesSource(vs);
        }

        for (InterpolationPostProcessor postProcessor : postProcessors) {
          interpolator.removePostProcessor(postProcessor);
        }
      }
    }

    return result;
  }

  protected RecursionInterceptor getRecursionInterceptor() {
    return recursionInterceptor;
  }

  protected void setRecursionInterceptor(RecursionInterceptor recursionInterceptor) {
    this.recursionInterceptor = recursionInterceptor;
  }

  protected abstract Interpolator createInterpolator();

  public void initialize() throws InitializationException {
    interpolator = createInterpolator();
    recursionInterceptor = new PrefixAwareRecursionInterceptor(PROJECT_PREFIXES);
  }

  protected final Interpolator getInterpolator() {
    return interpolator;
  }

}
